//--------------------------------------------------------------------------
// 
//  Copyright (c) Chili Software.  All rights reserved. 
// 
//  File: ChecksumBuilder.cs
//
//  Description: Creates check sums for persistent objects.
// 
//--------------------------------------------------------------------------

//#define DEBUG_CHECKSUM

using System;
using System.Collections.Generic;
using System.Text;
using System.Security.Cryptography;
using System.Globalization;
using Chili.Opf3.Mapping;

namespace Chili.Opf3
{
	/// <summary>
	/// This class is used to generate the checksums for persistent objects.
	/// </summary>
	internal sealed class ChecksumBuilder
	{
		#region ByteArrayBuilder

		/// <summary>
		/// Builds an array of bytes.
		/// </summary>
		private sealed class ByteArrayBuilder
		{
			private static readonly Encoding ENCODING = new UnicodeEncoding();
			private List<byte> _bytes = new List<byte>();
			private StringBuilder _strings = new StringBuilder();

			/// <summary>
			/// Appends a string to the byte array.
			/// </summary>
			/// <param name="value">The value to be added.</param>
			public void Append(string value)
			{
				if (value == null)
					return;

				_strings.Append(value);
				_strings.Append(value.Length.ToString());
			}

			/// <summary>
			/// Appends the bytes to the byte array.
			/// </summary>
			/// <param name="value">The value to be added.</param>
			public void Append(byte[] value)
			{
				if (value == null)
					return;

				_bytes.AddRange(value);
				_bytes.AddRange(BitConverter.GetBytes(value.Length));
			}

			/// <summary>
			/// Returns a byte array.
			/// </summary>
			/// <returns></returns>
			public byte[] ToByteArray()
			{
				// Add the bytes of the strings to the list.
				_bytes.AddRange(ENCODING.GetBytes(_strings.ToString()));

				// Return an array of bytes.
				return _bytes.ToArray();
			}
		}

		#endregion

		private static readonly Encoding ENCODING = new UnicodeEncoding();
		private static readonly CultureInfo US_CULTURE = new CultureInfo("en-US");
		private ByteArrayBuilder _checksumData = new ByteArrayBuilder(); // ByteArrayBuilder that contains the MD5.

		/// <summary>
		/// Creates a new instance of the <see cref="ChecksumBuilder">ChecksumBuilder Class</see>.
		/// </summary>
		public ChecksumBuilder()
		{
#if DEBUG_CHECKSUM
			// Debug code!
			Tracing.Current.TraceVerbose("Creating ChecksumBuilder Class.");
#endif
		}

		/// <summary>
		/// Calculates the checksum for the given persistent object.
		/// </summary>
		/// <param name="obj">Object thats checksum is calculated.</param>
		public static byte[] CalculatePersistentChecksum(object obj)
		{
			ChecksumBuilder builder = new ChecksumBuilder();

			// Get the mapping for the object.
			TypeMapping typeMapping = TypeMapping.GetTypeMapping(obj);

			// Only add the members if more then load is specified.
			if (typeMapping.PersistentAttribute.Rights > PersistentRights.Load)
			{
				foreach (IMemberInfo info in typeMapping.Members)
				{
					// Add the checksum data for the member.
					builder.AddMember(info.GetValue(obj), info.FieldAttribute);
				}

				IDynamicExtensible dynamicExtensible = obj as IDynamicExtensible;
				if (dynamicExtensible != null)
				{
					foreach (KeyValuePair<string, object> field in dynamicExtensible.UnknownFields)
					{
						builder.AddMember(field.Value, null);
					}
				}
			}

			return builder.ToChecksum();
		}

		/// <summary>
		/// Converts the content of the builder to a checksum.
		/// </summary>
		public byte[] ToChecksum()
		{
			byte[] bytes = _checksumData.ToByteArray();
			if (bytes.Length == 0)
				return null;

			// Create the checksum.
			return new MD5CryptoServiceProvider().ComputeHash(bytes);
		}

		/// <summary>
		/// Adds the member data to the checksum data. 
		/// </summary>
		/// <param name="value">Value that is added to the checksum data.</param>
		/// <param name="fieldAttribute">The <see cref="FieldAttribute">FieldAttribute</see> associated with the member.</param>
		public void AddMember(object value, FieldAttribute fieldAttribute)
		{
			if (value == null)
				return;

			if (fieldAttribute != null)
			{
				// Skip fields that aren't inserted nor updated.
				if ((fieldAttribute.PersistBehaviours & (PersistBehaviours.DontInsert | PersistBehaviours.DontUpdate)) ==
					(PersistBehaviours.DontInsert | PersistBehaviours.DontUpdate))
				{
					return;
				}
			}

			// --- FAST converting here ---

			string s = value as string;
			if (s != null)
			{
				// HACK: Replace line breaks with \n. Required for some storages (for example VistaDB).
				s = s.Replace("\r\n", "\n");
				_checksumData.Append(s);

#if DEBUG_CHECKSUM
				// Debug code!
				Tracing.Current.TraceVerbose(string.Format("{0} ({1}): {2}", fieldAttribute.FieldName, s.GetType(), s));
#endif
				return;
			}

			// Check if the value is of type int.
			if (value is int)
			{
				byte[] bytes = BitConverter.GetBytes((int)value);
				_checksumData.Append(bytes);

#if DEBUG_CHECKSUM
				// Debug code!
				Tracing.Current.TraceVerbose(string.Format("{0} ({1}): {2}", fieldAttribute.FieldName, value.GetType(), value));
#endif
				return;
			}

			// Check if the value is of type bool.
			if (value is bool)
			{
				byte[] bytes = BitConverter.GetBytes((bool)value);
				_checksumData.Append(bytes);

#if DEBUG_CHECKSUM
				// Debug code!
				Tracing.Current.TraceVerbose(string.Format("{0} ({1}): {2}", fieldAttribute.FieldName, value.GetType(), value));
#endif
				return;
			}

			// Check if the value is of type double.
			if (value is double)
			{
				byte[] bytes = BitConverter.GetBytes((double)value);
				_checksumData.Append(bytes);

#if DEBUG_CHECKSUM
				// Debug code!
				Tracing.Current.TraceVerbose(string.Format("{0} ({1}): {2}", fieldAttribute.FieldName, value.GetType(), value));
#endif
				return;
			}

			// Check if the value is of type short.
			if (value is short)
			{
				byte[] bytes = BitConverter.GetBytes((short)value);
				_checksumData.Append(bytes);

#if DEBUG_CHECKSUM
				// Debug code!
				Tracing.Current.TraceVerbose(string.Format("{0} ({1}): {2}", fieldAttribute.FieldName, value.GetType(), value));
#endif
				return;
			}

			// Check if the value is of type long.
			if (value is long)
			{
				byte[] bytes = BitConverter.GetBytes((long)value);
				_checksumData.Append(bytes);

#if DEBUG_CHECKSUM
				// Debug code!
				Tracing.Current.TraceVerbose(string.Format("{0} ({1}): {2}", fieldAttribute.FieldName, value.GetType(), value));
#endif
				return;
			}

			// Check if the value is of type Single.
			if (value is Single)
			{
				byte[] bytes = BitConverter.GetBytes((Single)value);
				_checksumData.Append(bytes);

#if DEBUG_CHECKSUM
				// Debug code!
				Tracing.Current.TraceVerbose(string.Format("{0} ({1}): {2}", fieldAttribute.FieldName, value.GetType(), value));
#endif
				return;
			}

			// Check if the value is a byte array.
			byte[] valueByte = value as byte[];
			if (valueByte != null)
			{
				_checksumData.Append(valueByte);

#if DEBUG_CHECKSUM
				// Debug code!
				Tracing.Current.TraceVerbose(string.Format("{0} ({1}): {2}", fieldAttribute.FieldName, valueByte.GetType(), valueByte));
#endif
				return;
			}



			// --- SLOWER converting here ---

			if (value is DateTime)
			{
				// We don't use milliseconds when calculating the datetime Md5 string.
				DateTime date = (DateTime)value;
				s = string.Format("{0}-{1}.{2}.{3}", date.Date, date.Hour, date.Minute, date.Second);
				_checksumData.Append(s);

#if DEBUG_CHECKSUM
				// Debug code!
				Tracing.Current.TraceVerbose(string.Format("{0} ({1}): {2}", fieldAttribute.FieldName, value.GetType(), s));
#endif
				return;
			}

			if (value is decimal)
			{
				s = ((decimal)value).ToString("F30", US_CULTURE);
				_checksumData.Append(s);

#if DEBUG_CHECKSUM
				// Debug code!
				Tracing.Current.TraceVerbose(string.Format("{0} ({1}): {2}", fieldAttribute.FieldName, value.GetType(), s));
#endif
				return;
			}

			// Enums are handled in a separate way.
			if (value is Enum)
			{
				byte[] bytes = null;
				if (Enum.GetUnderlyingType(value.GetType()) == typeof(int))
				{
					bytes = BitConverter.GetBytes((int)value);
#if DEBUG_CHECKSUM
					// Debug code!
					Tracing.Current.TraceVerbose(string.Format("{0} ({1}): {2}", fieldAttribute.FieldName, value.GetType(), (int)value));
#endif
				}
				else
				{
					bytes = BitConverter.GetBytes((long)value);
#if DEBUG_CHECKSUM
					// Debug code!
					Tracing.Current.TraceVerbose(string.Format("{0} ({1}): {2}", fieldAttribute.FieldName, value.GetType(), (long)value));
#endif
				}

				_checksumData.Append(bytes);
				return;
			}

			// Fall back to the slowest type of converting.
			s = value.ToString();
			_checksumData.Append(s);

#if DEBUG_CHECKSUM
			// Debug code!
			Tracing.Current.TraceVerbose(string.Format("{0} ({1}): {2}", fieldAttribute.FieldName, value.GetType(), s));
#endif
		}

		/// <summary>
		/// Checks if two checksums are equal.
		/// </summary>
		/// <param name="checksum1"></param>
		/// <param name="checksum2"></param>
		public static bool EqualsChecksum(byte[] checksum1, byte[] checksum2)
		{
			if (checksum1 == null && checksum2 != null)
				return false;
			if (checksum2 == null && checksum1 == null)
				return false;

			if (checksum1.Length != checksum2.Length)
				return false;

			int length = checksum1.Length;
			for (int i = 0; i < length; i++)
			{
				if (checksum1[i] != checksum2[i])
					return false;
			}
			return true;
		}
	}
}
